package com.wei.czz.framework.index.manager;

import com.google.code.kaptcha.Producer;
import com.wei.czz.common.constant.Constant;
import com.wei.czz.common.constant.EntityConstant;
import com.wei.czz.common.constant.RedisConstant;
import com.wei.czz.common.dto.admin.userRisk.UserRiskDto;
import com.wei.czz.common.dto.common.sms.SendSmsDto;
import com.wei.czz.common.dto.index.login.CaptchaDto;
import com.wei.czz.common.dto.index.login.LoginDto;
import com.wei.czz.common.enums.CommonEnum;
import com.wei.czz.common.enums.ResultEnum;
import com.wei.czz.common.enums.admin.user.LoginTypeEnum;
import com.wei.czz.common.enums.admin.user.RequestChannelEnum;
import com.wei.czz.common.enums.common.sms.SmsTypeEnum;
import com.wei.czz.common.enums.index.validateCode.ValidateCodeStatusEnum;
import com.wei.czz.common.enums.index.validateCode.ValidateCodeTypeEnum;
import com.wei.czz.common.exception.CzzException;
import com.wei.czz.common.po.ValidateCodePo;
import com.wei.czz.common.po.common.sms.SendSmsPo;
import com.wei.czz.common.property.AccessTokenProperty;
import com.wei.czz.common.utils.*;
import com.wei.czz.common.vo.index.sign.FastLoginVo;
import com.wei.czz.common.vo.index.sign.FindPwdVo;
import com.wei.czz.common.vo.index.sign.PwdLoginVo;
import com.wei.czz.framework.admin.entity.*;
import com.wei.czz.framework.admin.helper.UserHelper;
import com.wei.czz.framework.admin.service.*;
import com.wei.czz.framework.common.entity.IpAddressEntity;
import com.wei.czz.framework.common.entity.ValidateCodeEntity;
import com.wei.czz.framework.common.helper.SmsSendHelper;
import com.wei.czz.framework.common.handler.redis.RedisHandler;
import com.wei.czz.framework.common.handler.redis.RedisStringHandler;
import com.wei.czz.framework.common.service.*;
import com.wei.czz.framework.security.manager.RequestLimitManager;
import lombok.AllArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.util.FastByteArrayOutputStream;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * Created by IntelliJ IDEA.
 *
 * @author wyw
 * date: 2024-06-10 11:55:54
 * className: UserLoginManager
 * version: 1.0
 * description:
 */
@Component
@AllArgsConstructor
public class UserLoginManager {

    private static final Logger log = LoggerFactory.getLogger(UserLoginManager.class);

    /**
     * 用户登录常量值
     */
    private static final String LOGIN = "login";

    private final AccessTokenProperty accessTokenProperty;

    private final UserService userService;

    private final UserLoginService userLoginService;

    private final UserAccessTokenService userAccessTokenService;

    private final UserLoginFlowService userLoginFlowService;

    private final UserRiskFlowService userRiskFlowService;

    private final ValidateCodeService validateCodeService;

    private final IpAddressService ipAddressService;

    private final HistoryPasswordService historyPasswordService;

    private final RequestLimitManager requestLimitManager;

    private final AsyncService asyncService;

    private final AesService aesService;

    private final TransactionService transactionService;

    private final UserHelper userHelper;

    private final RedisStringHandler redisStringHandler;

    private final RedisHandler redisHandler;

    private final SnowflakeService snowflakeService;

    private final SmsSendHelper smsSendHelper;

    private final Producer producer;


    public CaptchaDto getImageCaptcha() {
        // 获取请求源的ip地址
        String requestIp = ContextUtils.get(Constant.REQUEST_IP);

        Date date = new Date();

        // 生成文字验证码
        String code = producer.createText();

        // 创建图片验证码
        BufferedImage bufferedImage = producer.createImage(code);

        // 存储图片验证码转换的字节数据流对象
        FastByteArrayOutputStream os = new FastByteArrayOutputStream();

        try {
            // 将验证码图片写入字节数据流中
            ImageIO.write(bufferedImage, "jpg", os);

        } catch (IOException e) {
            throw new CzzException("生成图片验证码失败，请务必及时处理");
        }

        // 计算验证码过期时间
        long expireTime = date.getTime() + Constant.FIVE_MINUTE_NUM;

        ValidateCodeEntity validateCode = new ValidateCodeEntity();
        validateCode.setAccount(Constant.IMAGE + snowflakeService.get())
                .setRequestIp(requestIp)
                .setType(ValidateCodeTypeEnum.IMAGE_AUTH.getValue())
                .setCode(code)
                .setExpireTime(expireTime)
                .setAuditTime(CommonEnum.ZERO.getLongValue())
                .setAuditStatus(ValidateCodeStatusEnum.NONE_AUTH.getValue())
                .setAuditFailNum(0)
                .setDeleteStatus(CommonEnum.ZERO.getValue())
                .setUpdateTime(date)
                .setUpdateUser(StringUtils.EMPTY)
                .setUpdateUserId(CommonEnum.ZERO.getLongValue())
                .setCreateTime(date)
                .setCreateUser(StringUtils.EMPTY)
                .setCreateUserId(CommonEnum.ZERO.getLongValue());
        validateCodeService.saveOrEdit(validateCode);

        ValidateCodePo validateCodePo = new ValidateCodePo();
        validateCodePo.setId(validateCode.getId())
                .setAccount(validateCode.getAccount())
                .setRequestIp(requestIp)
                .setCode(code)
                .setExpireTime(expireTime);

        // 获取缓存键
        String cacheKey = CacheKeyUtils.getValidateCodeKey(validateCode.getAccount(),
                ValidateCodeTypeEnum.IMAGE_AUTH.getValue());
        // 操作Redis缓存，新增验证码缓存信息
        redisStringHandler.set(cacheKey, validateCodePo,
                RandomNumUtils.randomNum(Constant.FIVE_MINUTE_NUM, 10000), TimeUnit.MILLISECONDS);

        CaptchaDto captchaDto = new CaptchaDto();
        captchaDto.setId(validateCode.getAccount())
                .setImage(Base64.getEncoder().encodeToString(os.toByteArray()))
                .setExpireTime(expireTime);
        return captchaDto;
    }

    public void sendLoginCode(String contact) {
        // 获取请求源的ip地址
        String requestIp = ContextUtils.get(Constant.REQUEST_IP);

        Date date = new Date();

        String mobile = null;
        String encryptMobile = null;
        String email = null;
        String encryptEmail = null;
        String message = ResultEnum.PARAM_FAIL.getMsg();

        if (RegexUtils.isPhone(contact)) {
            mobile = contact;
            encryptMobile = aesService.encryptToHex(contact);

            message = "网站不存在手机号，请确认，如有问题，请联系管理员处理";
        }
        if (RegexUtils.isEmail(contact)) {
            email = contact;
            encryptEmail = aesService.encryptToHex(contact);

            message = "网站不存在邮箱号，请确认，如有问题，请联系管理员处理";
        }

        /*
            判断联系方式是否存在
         */
        List<UserEntity> userList = userService.findUserList(encryptEmail, encryptMobile, null);
        if (userList.isEmpty()) {
            throw new CzzException(ResultEnum.REQUEST_FAIL.getCode(), message);
        }

        /*
            发送短信验证码
         */
        if (StringUtils.isNotBlank(mobile)) {
            // 获取随机验证码
            String code = RandomNumUtils.getRandomSixCode();

            /*
                缓存短信发送记录
             */
            String smsLogKey = CacheKeyUtils.getSmsLogKey(requestIp, SmsTypeEnum.LOGIN.getValue());
            //
            Boolean bool = redisStringHandler.setIfAbsent(smsLogKey, code, 1, TimeUnit.MINUTES);
            log.info("判断是否允许发送登录短信完成。bool={}", bool);
            if (Boolean.FALSE.equals(bool)) {
                message = "一分钟内不允许重复发送登录验证码，请稍后重试";
                throw new CzzException(ResultEnum.REQUEST_FAIL.getCode(), message);
            }

            /*
                发送短信验证码
             */
            SendSmsPo sendSmsPo = new SendSmsPo();
            sendSmsPo.setAccount(requestIp)
                    .setType(SmsTypeEnum.LOGIN.getValue())
                    .setMobile(mobile)
                    .setParamMap(Collections.singletonMap("code", code));
            try {

                SendSmsDto sendSmsDto = smsSendHelper.sendSms(sendSmsPo);
                if (CommonEnum.ONE.getValue().equals(sendSmsDto.getStatus())) {
                    throw new CzzException();
                }

            } catch (Exception e) {
                // 发送失败，立即删除缓存
                redisHandler.delete(smsLogKey);

                throw e;
            }

            // 计算验证码过期时间（十分钟）
            long expireTime = date.getTime() + Constant.MINUTE_NUM * 10;

            ValidateCodeEntity validateCode = new ValidateCodeEntity();
            validateCode.setAccount(encryptMobile)
                    .setRequestIp(requestIp)
                    .setType(ValidateCodeTypeEnum.FAST_LOGIN.getValue())
                    .setCode(code)
                    .setExpireTime(expireTime)
                    .setAuditTime(CommonEnum.ZERO.getLongValue())
                    .setAuditStatus(ValidateCodeStatusEnum.NONE_AUTH.getValue())
                    .setAuditFailNum(0)
                    .setDeleteStatus(CommonEnum.ZERO.getValue())
                    .setUpdateTime(date)
                    .setUpdateUser(StringUtils.EMPTY)
                    .setUpdateUserId(CommonEnum.ZERO.getLongValue())
                    .setCreateTime(date)
                    .setCreateUser(StringUtils.EMPTY)
                    .setCreateUserId(CommonEnum.ZERO.getLongValue());
            validateCodeService.saveOrEdit(validateCode);

            ValidateCodePo validateCodePo = new ValidateCodePo();
            validateCodePo.setId(validateCode.getId())
                    .setAccount(encryptMobile)
                    .setRequestIp(requestIp)
                    .setCode(code)
                    .setExpireTime(expireTime);

            // 获取缓存键
            String cacheKey = CacheKeyUtils.getValidateCodeKey(encryptMobile, ValidateCodeTypeEnum.FAST_LOGIN.getValue());
            // 操作Redis缓存，新增验证码缓存信息
            redisStringHandler.set(cacheKey, validateCodePo,
                    RandomNumUtils.randomNum(Constant.FIVE_MINUTE_NUM, 10000), TimeUnit.MILLISECONDS);
        }

        /*
            发送邮件验证码
         */
        if (StringUtils.isNotBlank(email)) {


        }
    }

    public void sendFindPasswordCode(String contact) {
        // 获取请求源的ip地址
        String requestIp = ContextUtils.get(Constant.REQUEST_IP);

        Date date = new Date();

        String mobile = null;
        String encryptMobile = null;
        String email = null;
        String encryptEmail = null;
        String message = ResultEnum.PARAM_FAIL.getMsg();

        if (RegexUtils.isPhone(contact)) {
            mobile = contact;
            encryptMobile = aesService.encryptToHex(contact);

            message = "网站不存在手机号，请确认，如有问题，请联系管理员处理";
        }
        if (RegexUtils.isEmail(contact)) {
            email = contact;
            encryptEmail = aesService.encryptToHex(contact);

            message = "网站不存在邮箱号，请确认，如有问题，请联系管理员处理";
        }

        /*
            判断联系方式是否存在
         */
        List<UserEntity> userList = userService.findUserList(encryptEmail, encryptMobile, null);
        if (userList.isEmpty()) {
            throw new CzzException(ResultEnum.REQUEST_FAIL.getCode(), message);
        }

        /*
            发送短信验证码
         */
        if (StringUtils.isNotBlank(mobile)) {
            // 获取随机验证码
            String code = RandomNumUtils.getRandomSixCode();

            /*
                缓存短信发送记录
             */
            String smsLogKey = CacheKeyUtils.getSmsLogKey(requestIp, SmsTypeEnum.FIND_PASSWORD.getValue());
            Boolean bool = redisStringHandler.setIfAbsent(smsLogKey, code, 1, TimeUnit.MINUTES);
            if (Boolean.FALSE.equals(bool)) {
                message = "一分钟内不允许重复发送登录验证码，请稍后重试";
                throw new CzzException(ResultEnum.REQUEST_FAIL.getCode(), message);
            }

            /*
                发送短信验证码
             */
            SendSmsPo sendSmsPo = new SendSmsPo();
            sendSmsPo.setAccount(requestIp)
                    .setType(SmsTypeEnum.FIND_PASSWORD.getValue())
                    .setMobile(mobile)
                    .setParamMap(Collections.singletonMap("code", code));
            try {

                SendSmsDto sendSmsDto = smsSendHelper.sendSms(sendSmsPo);
                if (CommonEnum.ONE.getValue().equals(sendSmsDto.getStatus())) {
                    throw new CzzException();
                }

            } catch (Exception e) {
                // 发送失败，立即删除缓存
                redisHandler.delete(smsLogKey);

                throw e;
            }

            // 计算验证码过期时间（十分钟）
            long expireTime = date.getTime() + Constant.MINUTE_NUM * 10;

            ValidateCodeEntity validateCode = new ValidateCodeEntity();
            validateCode.setAccount(encryptMobile)
                    .setRequestIp(requestIp)
                    .setType(ValidateCodeTypeEnum.FIND_PASSWORD.getValue())
                    .setCode(code)
                    .setExpireTime(expireTime)
                    .setAuditTime(CommonEnum.ZERO.getLongValue())
                    .setAuditStatus(ValidateCodeStatusEnum.NONE_AUTH.getValue())
                    .setAuditFailNum(0)
                    .setDeleteStatus(CommonEnum.ZERO.getValue())
                    .setUpdateTime(date)
                    .setUpdateUser(StringUtils.EMPTY)
                    .setUpdateUserId(CommonEnum.ZERO.getLongValue())
                    .setCreateTime(date)
                    .setCreateUser(StringUtils.EMPTY)
                    .setCreateUserId(CommonEnum.ZERO.getLongValue());
            validateCodeService.saveOrEdit(validateCode);

            ValidateCodePo validateCodePo = new ValidateCodePo();
            validateCodePo.setId(validateCode.getId())
                    .setAccount(encryptMobile)
                    .setRequestIp(requestIp)
                    .setCode(code)
                    .setExpireTime(expireTime);

            // 获取缓存键
            String cacheKey = CacheKeyUtils.getValidateCodeKey(encryptMobile, ValidateCodeTypeEnum.FIND_PASSWORD.getValue());
            // 操作Redis缓存，新增验证码缓存信息
            redisStringHandler.set(cacheKey, validateCodePo,
                    RandomNumUtils.randomNum(Constant.FIVE_MINUTE_NUM, 10000), TimeUnit.MILLISECONDS);
        }

        /*
            发送邮件验证码
         */
        if (StringUtils.isNotBlank(email)) {

        }
    }

    public LoginDto passwordLogin(PwdLoginVo pwdLoginVo) {
        // 登录渠道
        RequestChannelEnum requestChannelEnum = pwdLoginVo.getRequestChannelEnum();
        Integer channel = requestChannelEnum.getValue();

        // 获取用户请求的ip地址
        String loginIP = ContextUtils.get(Constant.REQUEST_IP);

        // 校验图片验证码
        validateCodeService.checkValidateCode(pwdLoginVo.getCodeId(), loginIP, ValidateCodeTypeEnum.IMAGE_AUTH,
                pwdLoginVo.getCode());

        String loginName = pwdLoginVo.getLoginName();
        String phone = null;
        String email = null;
        if (RegexUtils.isPhone(loginName)) {
            phone = loginName;
            phone = aesService.encryptToHex(phone);

            loginName = null;
        } else if (RegexUtils.isEmail(loginName)) {
            email = loginName;
            email = aesService.encryptToHex(email);

            loginName = null;
        }

        /*
            获取用户
         */
        List<UserEntity> userList = userService.findUserList(email, phone, loginName);
        if (userList.isEmpty()) {
            throw new CzzException(ResultEnum.REQUEST_FAIL.getCode(), "用户不存在，请确认");
        }
        UserEntity user = userList.get(0);
        log.info("密码登录，登录用户：{}", user);
        if (user.getStatus().equals(CommonEnum.ONE.getValue())) {
            log.info("用户被禁用");
            throw new CzzException(ResultEnum.REQUEST_FAIL.getCode(), "很抱歉，您已被系统禁用使用");
        }

        // 登录密码解密（凯撒移位解密）
        String password = CaesarUtils.decodePwd(pwdLoginVo.getPassword());
        // 登录密码加密
        password = ShaUtils.encodeSha256(password, user.getSalt());
        if (!password.equals(user.getPassword())) {
            log.info("用户登录密码错误");
            asyncService.execute(() -> {
                // 用户密码登录失败，风控登记登录失败记录
                requestLimitManager.userLoginFail(user.getUserId());
            });

            throw new CzzException(ResultEnum.PASSWORD_FAIL);
        }

        // 获取用户请求的现实地址
        IpAddressEntity ipAddress = ipAddressService.getIpAddress(loginIP);
        String requestAddress = ipAddress.getRequestAddress();

        /*
            登录缓存，一分钟只能登录一次
         */
        String cacheKey = RedisConstant.STRING + EntityConstant.USER + RedisConstant.SPLIT + LOGIN + RedisConstant.SPLIT
                + user.getUserId() + RedisConstant.SPLIT + channel;
        boolean bool = redisStringHandler.setIfAbsent(cacheKey, Boolean.TRUE, 1, TimeUnit.MINUTES);
        if (!bool) {
            String message = "登录失败，其他方式正在登录中，请确认后重试";
            throw new CzzException(ResultEnum.REQUEST_FAIL.getCode(), message);
        }

        // 获取当前系统时间（时间戳）
        long now = System.currentTimeMillis();
        Date date = new Date(now);

        LoginDto loginDto = new LoginDto();
        try {
            /*
                获取用户登录对象
             */
            UserLoginEntity userLogin = this.getUserLogin(user, channel, LoginTypeEnum.ACCOUNT.getValue());
            // 判断用户是否登录中
            boolean isLogin = CommonEnum.ZERO.getValue().equals(userLogin.getStatus())
                    && userLogin.getExpireTime() > now;
            if (isLogin) {
                // TODO 判断上一次登录地址是否和当前登录地址相差不大，相差大则报异常
                // TODO 暂时设定为允许登录，顶掉其他登录
            }

            /*
                生成用户令牌
             */
            // 获取令牌过期时间
            Long accessTokenExpire = accessTokenProperty.getAccessTokenExpire(requestChannelEnum);
            long fAccessTokenExpire = now + accessTokenExpire;
            // 获取刷新令牌过期时间
            Long refreshTokenExpire = accessTokenProperty.getRefreshTokenExpire(requestChannelEnum);
            long fRefreshTokenExpire = now + refreshTokenExpire;


            String sUserId = user.getUserId().toString();

            // 生成访问令牌
            String accessToken = aesService.czzEncrypt(sUserId);
            // 生成刷新令牌
            String refreshToken = aesService.czzEncrypt(sUserId);

            transactionService.execute(() -> {
                /*
                    保存用户登录记录
                 */
                userLogin.setType(LoginTypeEnum.ACCOUNT.getValue())
                        .setLoginIp(loginIP)
                        .setLoginAddress(requestAddress)
                        .setLoginTime(now)
                        .setExpireTime(fRefreshTokenExpire)
                        .setStatus(CommonEnum.ZERO.getValue());
                if (Objects.nonNull(userLogin.getId())) {
                    userLogin.setUpdateTime(date)
                            .setUpdateUser(user.getUsername())
                            .setUpdateUserId(user.getUserId());
                }
                userLoginService.saveOrEdit(userLogin);

                /*
                    保存用户令牌
                 */

                UserAccessTokenEntity saveUserAccessToken = new UserAccessTokenEntity();
                saveUserAccessToken.setUserId(user.getUserId())
                        .setChannel(channel)
                        .setAccessToken(accessToken)
                        .setAccessExpireTime(fAccessTokenExpire)
                        .setRefreshToken(refreshToken)
                        .setRefreshExpireTime(fRefreshTokenExpire);
                userAccessTokenService.saveOrEdit(saveUserAccessToken);
            });

            /*
                删除登录用户缓存
             */
            cacheKey = CacheKeyUtils.getSecurityUserKey(user.getUserId());
            redisHandler.delete(cacheKey);

            // 设置返回数据
            loginDto.setUsername(user.getUsername())
                    .setAvatar(user.getAvatar())
                    .setToken(accessToken)
                    .setExpireTime(fAccessTokenExpire)
                    .setAccessToken(accessToken)
                    .setAccessTokenExpire(fAccessTokenExpire)
                    .setRefreshToken(refreshToken)
                    .setRefreshTokenExpire(fRefreshTokenExpire);

        } catch (CzzException e) {
            throw e;
        } catch (Exception e) {
            log.info("用户登录数据生成异常。message={}", e.getMessage(), e);

            throw new CzzException(e);
        } finally {
            long end = System.currentTimeMillis();
            if (end - now < Constant.MINUTE_NUM) {
                // 登录完成，立即解锁
                redisHandler.delete(cacheKey);
            }
        }

        asyncService.execute(() -> {
            /*
                异步保存用户登录记录
             */
            UserLoginFlowEntity userLoginFlow = new UserLoginFlowEntity();
            userLoginFlow.setUserId(user.getUserId())
                    .setChannel(channel)
                    .setType(LoginTypeEnum.ACCOUNT.getValue())
                    .setLoginIp(loginIP)
                    .setLoginAddress(requestAddress)
                    .setLogoutTime(CommonEnum.ZERO.getLongValue())
                    .setDeleteStatus(CommonEnum.ZERO.getValue())
                    .setUpdateTime(date)
                    .setUpdateUser(StringUtils.EMPTY)
                    .setUpdateUserId(CommonEnum.ZERO.getLongValue())
                    .setCreateTime(date)
                    .setCreateUser(user.getUsername())
                    .setCreateUserId(user.getUserId());
            userLoginFlowService.saveOrEdit(userLoginFlow);

            /*
                清理用户其他令牌
             */
            userAccessTokenService.clearOther(user.getUserId(), channel, loginDto.getRefreshToken());

            /*
                异步清理用户登录失败次数
             */
            requestLimitManager.resetSignFail(user.getUserId());
        });

        return loginDto;
    }

    public LoginDto fastLogin(FastLoginVo fastLoginVo) {
        // 登录渠道
        RequestChannelEnum requestChannelEnum = fastLoginVo.getRequestChannelEnum();
        Integer channel = requestChannelEnum.getValue();

        // 获取用户请求的ip地址
        String loginIP = ContextUtils.get(Constant.REQUEST_IP);

        // 联系方式加密
        String contact = aesService.encryptToHex(fastLoginVo.getContact());

        // 校验验证码
        validateCodeService.checkValidateCode(contact, loginIP, ValidateCodeTypeEnum.FAST_LOGIN, fastLoginVo.getCode());

        String phone = null;
        String email = null;
        Integer loginType;
        if (RegexUtils.isPhone(fastLoginVo.getContact())) {
            phone = contact;

            loginType = LoginTypeEnum.PHONE.getValue();
        } else if (RegexUtils.isEmail(fastLoginVo.getContact())) {
            email = contact;

            loginType = LoginTypeEnum.EMAIL.getValue();
        } else {
            loginType = null;
        }


        /*
            获取用户
         */
        List<UserEntity> userList = userService.findUserList(email, phone, null);
        if (userList.isEmpty()) {
            throw new CzzException(ResultEnum.REQUEST_FAIL.getCode(), "用户不存在，请确认");
        }
        UserEntity user = userList.get(0);
        log.info("免密登录，登录用户: {}", user);

        if (user.getStatus().equals(CommonEnum.ONE.getValue())) {
            log.info("免密登录，用户被禁用");
            throw new CzzException(ResultEnum.REQUEST_FAIL.getCode(), "很抱歉，您已被系统禁用使用");
        }

        // 获取用户请求的现实地址
        IpAddressEntity ipAddress = ipAddressService.getIpAddress(loginIP);
        String requestAddress = ipAddress.getRequestAddress();

        /*
            登录缓存，一分钟只能登录一次
         */
        String cacheKey = RedisConstant.STRING + EntityConstant.USER + RedisConstant.SPLIT + LOGIN + RedisConstant.SPLIT
                + user.getUserId() + RedisConstant.SPLIT + channel;
        boolean bool = redisStringHandler.setIfAbsent(cacheKey, Boolean.TRUE, 1, TimeUnit.MINUTES);
        if (!bool) {
            String message = "登录失败，其他方式正在登录中，请确认后重试";
            throw new CzzException(ResultEnum.REQUEST_FAIL.getCode(), message);
        }

        // 获取当前系统时间（时间戳）
        long now = System.currentTimeMillis();
        Date date = new Date(now);

        LoginDto loginDto = new LoginDto();

        try {
            /*
                获取用户登录对象
             */
            UserLoginEntity userLogin = this.getUserLogin(user, channel, loginType);

            /*
                生成用户令牌
             */
            // 获取令牌过期时间
            Long accessTokenExpire = accessTokenProperty.getAccessTokenExpire(requestChannelEnum);
            long fAccessTokenExpire = now + accessTokenExpire;
            // 获取刷新令牌过期时间
            Long refreshTokenExpire = accessTokenProperty.getRefreshTokenExpire(requestChannelEnum);
            long fRefreshTokenExpire = now + refreshTokenExpire;

            String sUserId = user.getUserId().toString();

            // 生成访问令牌
            String accessToken = aesService.czzEncrypt(sUserId);
            // 生成刷新令牌
            String refreshToken = aesService.czzEncrypt(sUserId);

            // 事务方式执行
            transactionService.execute(() -> {
                /*
                    保存用户登录记录
                 */
                userLogin.setType(loginType)
                        .setLoginIp(loginIP)
                        .setLoginAddress(requestAddress)
                        .setLoginTime(now)
                        .setExpireTime(fRefreshTokenExpire)
                        .setStatus(CommonEnum.ZERO.getValue());
                if (Objects.nonNull(userLogin.getId())) {
                    userLogin.setUpdateTime(date)
                            .setUpdateUser(user.getUsername())
                            .setUpdateUserId(user.getUserId());
                }
                userLoginService.saveOrEdit(userLogin);

                /*
                    保存用户令牌
                 */
                UserAccessTokenEntity saveUserAccessToken = new UserAccessTokenEntity();
                saveUserAccessToken.setUserId(user.getUserId())
                        .setChannel(channel)
                        .setAccessToken(accessToken)
                        .setAccessExpireTime(fAccessTokenExpire)
                        .setRefreshToken(refreshToken)
                        .setRefreshExpireTime(fRefreshTokenExpire);
                userAccessTokenService.saveOrEdit(saveUserAccessToken);
            });

            /*
                删除登录用户缓存
             */
            cacheKey = CacheKeyUtils.getSecurityUserKey(user.getUserId());
            redisHandler.delete(cacheKey);

            // 设置返回数据
            loginDto.setUsername(user.getUsername())
                    .setAvatar(user.getAvatar())
                    .setToken(accessToken)
                    .setExpireTime(fAccessTokenExpire)
                    .setAccessToken(accessToken)
                    .setAccessTokenExpire(fAccessTokenExpire)
                    .setRefreshToken(refreshToken)
                    .setRefreshTokenExpire(fRefreshTokenExpire);

        } catch (CzzException e) {
            throw e;
        } catch (Exception e) {
            log.error("用户登录数据生成异常。message={}", e.getMessage(), e);

            throw new CzzException(e);
        } finally {
            long end = System.currentTimeMillis();
            if (end - now < Constant.MINUTE_NUM) {
                // 登录完成，立即解锁
                redisHandler.delete(cacheKey);
            }
        }

        asyncService.execute(() -> {
            /*
                异步保存用户登录记录
             */
            UserLoginFlowEntity userLoginFlow = new UserLoginFlowEntity();
            userLoginFlow.setUserId(user.getUserId())
                    .setChannel(channel)
                    .setType(loginType)
                    .setLoginIp(loginIP)
                    .setLoginAddress(requestAddress)
                    .setLogoutTime(CommonEnum.ZERO.getLongValue())
                    .setDeleteStatus(CommonEnum.ZERO.getValue())
                    .setUpdateTime(date)
                    .setUpdateUser(StringUtils.EMPTY)
                    .setUpdateUserId(CommonEnum.ZERO.getLongValue())
                    .setCreateTime(date)
                    .setCreateUser(user.getUsername())
                    .setCreateUserId(user.getUserId());
            userLoginFlowService.saveOrEdit(userLoginFlow);

            /*
                清理用户其他令牌
             */
            userAccessTokenService.clearOther(user.getUserId(), channel, loginDto.getRefreshToken());
        });

        return loginDto;
    }

    public void findPassword(FindPwdVo findPwdVo) {
        // 获取用户请求的ip地址
        String requestIp = ContextUtils.get(Constant.REQUEST_IP);

        Date date = new Date();

        // 联系方式加密
        String contact = aesService.encryptToHex(findPwdVo.getContact());

        // 校验验证码
        validateCodeService.checkValidateCode(contact, requestIp, ValidateCodeTypeEnum.FIND_PASSWORD, findPwdVo.getCode());

        String phone = null;
        String email = null;
        if (RegexUtils.isPhone(findPwdVo.getContact())) {
            phone = contact;

        } else if (RegexUtils.isEmail(findPwdVo.getContact())) {
            email = contact;

        }

        /*
            获取用户
         */
        List<UserEntity> userList = userService.findUserList(email, phone, null);
        if (userList.isEmpty()) {
            throw new CzzException(ResultEnum.REQUEST_FAIL.getCode(), "用户不存在，请确认");
        }
        UserEntity user = userList.get(0);
        log.info("找回密码用户： {}", user);

        if (user.getStatus().equals(CommonEnum.ONE.getValue())) {
            log.info("{}，用户被禁用。id={}", ValidateCodeTypeEnum.FIND_PASSWORD.getName(), user.getUserId());
            throw new CzzException(ResultEnum.REQUEST_FAIL.getCode(), "很抱歉，您已被系统禁用使用");
        }

        // 新密码解密（凯撒移位算法解密）
        String newPassword = CaesarUtils.decodePwd(findPwdVo.getPassword());
        // 新密码加密
        newPassword = ShaUtils.encodeSha256(newPassword, user.getSalt());
//        System.out.println("用户密码:" + newPassword);

        // 获取用户最近使用过程前三个登录密码
        List<String> oldPasswordList = historyPasswordService.getFirst3OldPasswordByUserId(user.getUserId());
        // 判断新密码是否与最近三次密码相同
        for (String oldPassword : oldPasswordList) {
            if (newPassword.equals(oldPassword)) {
                log.info("用户新密码与最近三次使用的密码相同。newPassword={} oldPassword={}", newPassword, oldPassword);
                throw new CzzException(ResultEnum.REQUEST_FAIL.getCode(), "新密码不能与最近三次使用的密码相同,请重新操作");
            }
        }
        // 新密码加密
        String password = newPassword;

        UserEntity updateUser = new UserEntity();
        updateUser.setUserId(user.getUserId())
                .setPassword(password)
                .setUpdateTime(date)
                .setUpdateUser(user.getUsername())
                .setUpdateUserId(user.getUserId());
        userService.saveOrEdit(updateUser);

        asyncService.execute(() -> {
            // 异步记录用户密码使用记录
            historyPasswordService.saveUserPassword(user.getUserId(), password);
        });
    }

    public UserRiskDto appealLogin(PwdLoginVo pwdLoginVo) {
        // 获取用户请求的ip地址
        String requestIp = ContextUtils.get(Constant.REQUEST_IP);

        // 校验验证码
        validateCodeService.checkValidateCode(pwdLoginVo.getCodeId(), requestIp, ValidateCodeTypeEnum.IMAGE_AUTH,
                pwdLoginVo.getCode());

        String loginName = pwdLoginVo.getLoginName();
        String phone = null;
        String email = null;
        if (RegexUtils.isPhone(loginName)) {
            phone = loginName;
            phone = aesService.encryptToHex(phone);

            loginName = null;
        } else if (RegexUtils.isEmail(loginName)) {
            email = loginName;
            email = aesService.encryptToHex(email);

            loginName = null;
        }

        /*
            获取用户
         */
        List<UserEntity> userList = userService.findUserList(email, phone, loginName);
        if (userList.isEmpty()) {
            throw new CzzException(ResultEnum.REQUEST_FAIL.getCode(), "用户不存在，请确认");
        }
        UserEntity user = userList.get(0);
        log.info("申诉登录，申诉用户：{}", user);
        if (user.getStatus().equals(CommonEnum.ZERO.getValue())) {
            // 用户状态为可用
            throw new CzzException(ResultEnum.REQUEST_FAIL.getCode(), "您账号没有任何问题，无需进行申诉操作。");
        }

        // 登录密码解密（凯撒移位解密）
        String password = CaesarUtils.decodePwd(pwdLoginVo.getPassword());
        // 登录密码加密
        password = ShaUtils.encodeSha256(password, user.getSalt());
        if (!password.equals(user.getPassword())) {
            log.info("申诉用户输入密码错误");
            throw new CzzException(ResultEnum.PASSWORD_FAIL);
        }

        /*
            获取用户风控信息列表
         */
        UserRiskFlowEntity userRisk = userRiskFlowService.findLastUserRiskFlow(user.getUserId());
        if (Objects.isNull(userRisk)) {
            log.info("用户风控记录为空。{}", user);
            throw new CzzException("用户状态不可用，但风控内容为空，请务必及时处理");
        }

        UserRiskDto userRiskDto = new UserRiskDto();
        userRiskDto.setId(userRisk.getId().toString())
                .setRiskType(userRisk.getRiskType())
                .setRiskReason(userRisk.getRiskReason());
        return userRiskDto;
    }

    public LoginDto refreshToken(String refreshToken, RequestChannelEnum requestChannelEnum) {

        /*
            用户令牌解密
         */
        String sUserId = aesService.czzDecrypt(refreshToken);

        long userId;
        try {
            userId = Long.parseLong(sUserId);
        } catch (NumberFormatException e) {
            log.warn("访问令牌携带的不是用户主键。_userId={}", sUserId);
            throw new CzzException(ResultEnum.TOKEN_INVALID);
        }

        /*
            获取用户
         */
        UserEntity user = userHelper.get(userId);
        if (Objects.isNull(user)) {
            log.warn("用户不存在。userId={}", userId);
            throw new CzzException(ResultEnum.ILLEGAL_FAIL);
        }
        if (user.getStatus().equals(CommonEnum.ONE.getValue())) {
            log.info("用户被禁用。userId={}", userId);
            throw new CzzException(ResultEnum.REQUEST_FAIL.getCode(), "很抱歉，您已被系统禁用使用");
        }

        /*
            刷新令牌缓存，一分钟只能刷新一次
         */
        String redisKey = RedisConstant.STRING + Constant.ACCESS_TOKEN + RedisConstant.SPLIT + refreshToken;
        Boolean bool = redisStringHandler.setIfAbsent(redisKey, Boolean.TRUE, 1, TimeUnit.MINUTES);
        if (!bool) {
            String message = "刷新token失败，失败原因：1）正在刷新中；2）刷新过于频繁，请稍后重试";
            throw new CzzException(ResultEnum.REQUEST_FAIL.getCode(), message);
        }

        try {

            /*
                获取用户令牌
             */
            List<UserAccessTokenEntity> userAccessTokenList = userAccessTokenService.findEffectList(userId, requestChannelEnum.getValue());
            //
            long now = System.currentTimeMillis();
            //
            Optional<UserAccessTokenEntity> optional = userAccessTokenList.stream()
                    .filter(userAccessToken -> userAccessToken.getChannel().equals(requestChannelEnum.getValue()))
                    .filter(userAccessToken -> userAccessToken.getRefreshExpireTime() > now)
                    .filter(userAccessToken -> userAccessToken.getRefreshToken().equals(refreshToken))
                    .findFirst();
            if (optional.isEmpty()) {
                log.warn("刷新令牌已过期或不存在。refreshToken={}", refreshToken);
                throw new CzzException(ResultEnum.NOT_LOGIN);
            }
            //
            UserAccessTokenEntity userAccessToken = optional.get();

            // 获取令牌过期时间
            Long accessTokenExpire = accessTokenProperty.getAccessTokenExpire(requestChannelEnum);
            long fAccessTokenExpire = now + accessTokenExpire;

            // 生成新访问令牌
            String accessToken = aesService.czzEncrypt(sUserId);

            // 判断是否需要更新刷新令牌
            boolean isUpdateRefreshToken = userAccessToken.getRefreshExpireTime() / 2 <= now;
            String newRefreshToken = refreshToken;
            long fRefreshTokenExpire = userAccessToken.getRefreshExpireTime();
            if (isUpdateRefreshToken) {
                // 生成新刷新令牌
                newRefreshToken = aesService.czzEncrypt(sUserId);

                // 获取刷新令牌过期时间
                Long refreshTokenExpire = accessTokenProperty.getRefreshTokenExpire(requestChannelEnum);
                fRefreshTokenExpire = now + refreshTokenExpire;
            }

            /*
                更新用户令牌
             */
            UserAccessTokenEntity updateUserAccessToken = new UserAccessTokenEntity();
            updateUserAccessToken.setId(userAccessToken.getId())
                    .setAccessToken(accessToken)
                    .setAccessExpireTime(fAccessTokenExpire);
            if (isUpdateRefreshToken) {
                updateUserAccessToken.setRefreshToken(newRefreshToken)
                        .setRefreshExpireTime(fRefreshTokenExpire);
            }
            userAccessTokenService.saveOrEdit(updateUserAccessToken);

            /*
                删除用户缓存
             */
            String cacheKey = CacheKeyUtils.getSecurityUserKey(userId);
            redisHandler.delete(cacheKey);

            // 封装结果
            LoginDto loginDto = new LoginDto();
            loginDto.setToken(accessToken)
                    .setExpireTime(fAccessTokenExpire)
                    .setAccessToken(accessToken)
                    .setAccessTokenExpire(fAccessTokenExpire)
                    .setRefreshToken(newRefreshToken)
                    .setRefreshTokenExpire(fRefreshTokenExpire);

            return loginDto;

        } catch (Exception e) {
            bool = false;

            if (e instanceof CzzException) {
                throw e;
            }
            log.info("刷新令牌异常。message={}", e.getMessage(), e);
            throw new CzzException(ResultEnum.SERVICE_FAIL);
        } finally {
            if (!bool) {
                // 刷新失败，立即解锁
                redisHandler.delete(redisKey);
            }
        }
    }

    /**
     * 获取用户登录对象
     * @param user    用户
     * @param channel 登录渠道
     * @param type    登录类型
     * @return 用户登录对象
     */
    private UserLoginEntity getUserLogin(UserEntity user, Integer channel, Integer type) {

        /*
            获取用户登录对象
         */
        List<UserLoginEntity> userLoginList = userLoginService.findList(user.getUserId(), channel);

        UserLoginEntity userLogin = null;
        if (!userLoginList.isEmpty()) {
            userLogin = userLoginList.get(userLoginList.size() - 1);
        }
        if (Objects.isNull(userLogin)) {
            userLogin = new UserLoginEntity();
            userLogin.setUserId(user.getUserId())
                    .setChannel(channel)
                    .setType(type)
                    .setLoginIp(StringUtils.EMPTY)
                    .setLoginAddress(StringUtils.EMPTY)
                    .setLoginTime(CommonEnum.ZERO.getLongValue())
                    .setExpireTime(CommonEnum.ZERO.getLongValue())
                    .setStatus(CommonEnum.ONE.getValue())
                    .setDeleteStatus(CommonEnum.ZERO.getValue())
                    .setUpdateTime(new Date())
                    .setUpdateUser(StringUtils.EMPTY)
                    .setUpdateUserId(CommonEnum.ZERO.getLongValue())
                    .setCreateTime(new Date())
                    .setCreateUser(user.getUsername())
                    .setCreateUserId(user.getUserId());
        }
        return userLogin;
    }
}
